(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.singleSpa = {}));
}(this, (function (exports) { 'use strict';

  // 描述应用的整个状态

  const NOT_LOADED = 'NOT_LOADED'; // 应用初始状态
  const LOADING_SOURCE_CODE = 'LOADING_SOURCE_CODE'; // 加载资源
  const NOT_BOOTSTRAPPED = 'NOT_BOOTSTRAPPED'; // 还没有调用bootstrap方法
  const BOOTSTRAPPING = 'BOOTSTRAPPING'; // 启动中
  const NOT_MOUNTED = 'NOT_MOUNTED'; // 没有调用 mount 方法
  const MOUNTING = 'MOUNTING'; // 正在挂载中
  const MOUNTED = 'MOUNTED'; // 挂载完毕
  const UPDATING = 'UPDATING'; // 更新中
  const UNMOUNTING = 'UNMOUNTING'; // 卸载中
  const LOAD_ERROR$1 = 'LOAD_ERROR'; // 
  const SKIP_BECAUSE_BROKEN = 'SKIP_BECAUSE_BROKEN'; // 

  // 当前这个应用是否需要被激活
  function shouldBeActive(app) {
    return app.activeWhen(window.location)
  }

  const isInBrowser = typeof window !== "undefined";

  let started = false;

  function start() {
    started = true;
    if (isInBrowser) {
      reroute(); // 除了去加载应用还需要挂载应用
    }
  }

  // 使用一个函数的话，外部只可以获取，而不能修改 started 这个值
  function isStarted () {
    return started
  }

  function flattenFnArray(fns) {
    fns = Array.isArray(fns) ? fns: [fns];
    return function(props) {
      return fns.reduce((p, fn) => p.then(() => fn(props)), Promise.resolve())
    }
  }

  async function toLoadPromise(app) {
    if (app.loadPromise) {
      return app.loadPromise
    }
    if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {
      return app
    }

    app.status = LOADING_SOURCE_CODE;

    return (app.loadPromise = Promise.resolve()
      .then(() => {
        const loadPromise = app.loadApp(app.customProps);
        return loadPromise.then(val => {
          app.loadErrorTime = null;
          app.status = NOT_BOOTSTRAPPED;
          app.bootstrap = flattenFnArray(val.bootstrap);
          app.mount = flattenFnArray(val.mount);
          app.unmount = flattenFnArray(val.unmount);
          delete app.loadPromise;
          return app
        })
      })
    )
  }

  async function toUnmountPromise(app) {
    if (app.status !== MOUNTED) return app
    app.status = UNMOUNTING;
    await app.unmount(app.customProps);
    app.status = NOT_MOUNTED;
    return app
  }

  async function toBootstrapPromise(app) {
    if (app.status !== NOT_BOOTSTRAPPED) return app
    app.status = BOOTSTRAPPING;
    await app.bootstrap();
    app.status = NOT_MOUNTED;
    return app
  }

  // hashchange popstate

  const routingEventsListeningTo = ['hashchange', 'popstate'];

  const captureEventisteners = {
    hashchange: [],
    popstate: []
  };

  function urlReroute() {
    reroute([]); // 会根据路径加载不同的应用
  }

  function patchedUpdateState(updateState, methodName) {
    return function() {
      const urlBefore = window.location.href;
      const result = updateState.apply(this, arguments); // 调用切换方法
      const urlAfter = window.location.href;
      if (urlBefore !== urlAfter) {
        if (isStarted()) {
          window.dispatchEvent(
            createPopStateEvent(window.history.state, methodName)
          );
        } else {
          reroute([]);
        }
      }
      return result
    }
  }

  function createPopStateEvent (state, originalMethodName) {
    let evt;
    try {
      evt = new PopStateEvent('popstate', { state });
    } catch (err) {
      evt = document.createEvent("PopStateEvent");
      evt.initPopStateEvent("popstate", false, false, state);
    }
    evt.singleSpa = true;
    evt.singleSpaTrigger = originalMethodName;
    return evt
  }

  if (isInBrowser) {
    window.addEventListener('hashchange', urlReroute);
    window.addEventListener('popstate', urlReroute);
    
    const originalAddEventListener = window.addEventListener;
    const originalRemoveEventListener = window.removeEventListener;
    
    window.addEventListener = function(eventName, fn) {
      if (typeof fn === 'function') {
        if (
          routingEventsListeningTo.indexOf(eventName) > -1 && 
          !captureEventisteners[eventName].some(listener => listener === fn)
        ) {
          captureEventisteners[eventName].push(fn);
          return
        }
      }
      return originalAddEventListener.apply(this, arguments)
    };
    window.removeEventListener = function(eventName, listenerFn) {
      if (typeof listenerFn === 'function') {
        if (routingEventsListeningTo.indexOf(eventName) > -1) {
          captureEventisteners[eventName] = captureEventisteners[eventName].filter(fn => fn !== listenerFn);
          return
        }
      }
      return originalRemoveEventListener.apply(this, arguments)
    };

    window.history.pushstate = patchedUpdateState(window.history.pushstate, 'pushstate');
    window.history.replacestate = patchedUpdateState(window.history.replacestate, 'replacestate');
  }

  // 用户可能还会绑定自己的路由事件
  // 当我们应用切换后，还需要执行原来的方法，需要在应用切换后继续执行

  function reroute(pendingPromises = [], eventArguments) {
    // 需要获取 要加载的应用
    // 需要获取 要被挂载的应用
    //  哪些应用需要被挂载
    const { appsToUnmount, appsToLoad, appsToMount } = getAppChanges();

    if (isStarted()) {
      // app 装载
      return performanceAppChanges() // 根据路径来装载应用
    } else {
      // 注册应用，需要预先加载
      return loadApps() // 预加载应用
    }

    function loadApps() { // 预加载应用
      return Promise.resolve().then(() => {
        const loadPromises = appsToLoad.map(toLoadPromise);
      })
    }
    async function performanceAppChanges() { // 根据路径来装载应用
      // 先卸载不用的应用
      let unmountPromise =  appsToUnmount.map(toUnmountPromise);
      // 去加载需要的应用
      appsToLoad.map(async (app) => { // 将需要加载的应用拿到 =》 启动 =》 挂载
        app = await toLoadPromise(app);
        app = await toBootstrapPromise(app);
        return await toMountPromise(app)
      });
      appsToMount.map(async (app) => {
        app = await toLoadPromise(app);
        return await toMountPromise(app)
      });
    }
  }

  const apps = []; // 用来存放所有的应用

  /**
   * 维护应用的所有状态 
   * @param {*} appName 应用名字
   * @param {*} loadApp 加载的应用
   * @param {*} activeWhen 当激活时会调用 loadApp
   * @param {*} customProps 自定义属性
   */
  function registerApplication(appNameOrConfig, appOrLoadApp, activeWhen, customProps) {
    const registration = sanitizeArguments(
      appNameOrConfig,
      appOrLoadApp,
      activeWhen,
      customProps
    );
    apps.push({
      ...registration,
      status: NOT_LOADED,
      loadErrorTime: null
    });
    if (isInBrowser) {
      reroute(); // 加载应用
    }
  }

  function getAppChanges() {
    const appsToUnload = [];
    const appsToUnmount = []; // 需要卸载的app
    const appsToLoad = []; // 需要被加载的app
    const appsToMount = []; // 需要被挂载的app

    // 我们会尝试 在LOAD_ERROR 200ms 后 再去下载应用
    // We re-attempt to download applications in LOAD_ERROR after a timeout of 200 milliseconds
    const currentTime = new Date().getTime();
    apps.forEach(app => {
      // 需不需要被加载
      const appShouldBeActive = app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app);

      switch(app.status) {
        case LOAD_ERROR$1:
          if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) {
            appsToLoad.push(app);
          }
          break
        case NOT_LOADED:
        case LOADING_SOURCE_CODE:
          if (appShouldBeActive) {
            appsToLoad.push(app);
          }
          break
        case NOT_BOOTSTRAPPED:
        case NOT_MOUNTED:
          if (appShouldBeActive) {
            appsToMount.push(app);
          }
          break
        case MOUNTED:
          if (!appShouldBeActive) {
            appsToUnmount.push(app);
          }
          break 
      }
    });
    return {
      appsToUnload,
      appsToUnmount,
      appsToLoad,
      appsToMount
    }
  }

  function sanitizeArguments (
    appNameOrConfig,
    appOrLoadApp,
    activeWhen,
    customProps
  ) {
    const usingObjectAPI = typeof appNameOrConfig === 'object';
    const registration = {
      name: null,
      loadApp: null,
      activeWhen: null,
      customProps: null
    };
    if (usingObjectAPI) {
      registration.name = appNameOrConfig.name;
      registration.loadApp = appNameOrConfig.app;
      registration.activeWhen = appNameOrConfig.activeWhen;
      registration.customProps = appNameOrConfig.customProps;
    } else {
      registration.name = appNameOrConfig;
      registration.loadApp = appOrLoadApp;
      registration.activeWhen = activeWhen;
      registration.customProps = customProps;
    }
    return registration
  }

  exports.BOOTSTRAPPING = BOOTSTRAPPING;
  exports.LOADING_SOURCE_CODE = LOADING_SOURCE_CODE;
  exports.LOAD_ERROR = LOAD_ERROR$1;
  exports.MOUNTED = MOUNTED;
  exports.MOUNTING = MOUNTING;
  exports.NOT_BOOTSTRAPPED = NOT_BOOTSTRAPPED;
  exports.NOT_LOADED = NOT_LOADED;
  exports.NOT_MOUNTED = NOT_MOUNTED;
  exports.SKIP_BECAUSE_BROKEN = SKIP_BECAUSE_BROKEN;
  exports.UNMOUNTING = UNMOUNTING;
  exports.UPDATING = UPDATING;
  exports.registerApplication = registerApplication;
  exports.start = start;

  Object.defineProperty(exports, '__esModule', { value: true });

})));
